#include <linux/module.h>
#include <linux/skbuff.h>
#include <net/ip.h>
#include <net/tcp.h>
#include <linux/netfilter_ipv4/ip_conntrack.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include "ipt_ZeSA.h"

#define	TH_FIN	0x01
#define	TH_SYN	0x02
#define	TH_RST	0x04
#define	TH_PUSH	0x08
#define	TH_ACK	0x10
#define	TH_URG	0x20
#define	TH_ECE	0x40
#define	TH_CWR	0x80

#define IP_DF			0x4000
#define TCPOPT_MSS		2
#define TCPOLEN_MSS		4

#define bm_max(a,b) (a>b?a:b)
#define IPT_STATE_BIT(ctinfo) (1 << ((ctinfo)%IP_CT_IS_REPLY+1))

//static unsigned int strmaxlen = 2048;
//MODULE_PARM(strmaxlen,"i");

static void connection_attach(struct sk_buff *new_skb, struct nf_ct_info *nfct)
{
	void (*attach)(struct sk_buff *, struct nf_ct_info *);
	if (nfct && (attach = ip_ct_attach) != NULL)
		attach(new_skb, nfct);
}

//send RST
static void send_reset(const struct sk_buff *oldskb, int local)
{
	struct sk_buff *nskb;
	struct tcphdr *otcph, *tcph;
	struct rtable *rt;
	unsigned int otcplen;
	u_int16_t tmp;
	int needs_ack;

	otcph = (struct tcphdr *)((u_int32_t*)oldskb->nh.iph + oldskb->nh.iph->ihl);
	otcplen = oldskb->len - oldskb->nh.iph->ihl*4;

	nskb = skb_copy(oldskb, GFP_ATOMIC);
	if (!nskb)
		return;

	/* Clear nf fields */
	nf_conntrack_put(nskb->nfct);
	nskb->nfct = NULL;
	nskb->nfcache = 0;
#ifdef CONFIG_NETFILTER_DEBUG
	nskb->nf_debug = 0;
#endif

	tcph = (struct tcphdr *)((u_int32_t*)nskb->nh.iph + nskb->nh.iph->ihl);

	/* Swap source and dest */
	nskb->nh.iph->daddr = xchg(&nskb->nh.iph->saddr, nskb->nh.iph->daddr);
	tmp = tcph->source;
	tcph->source = tcph->dest;
	tcph->dest = tmp;

	/* Truncate to length (no data) */
	tcph->doff = sizeof(struct tcphdr)/4;
	skb_trim(nskb, nskb->nh.iph->ihl*4 + sizeof(struct tcphdr));
	nskb->nh.iph->tot_len = htons(nskb->len);

	if (tcph->ack) {
		needs_ack = 0;
		tcph->seq = otcph->ack_seq;
		tcph->ack_seq = 0;
	} else {
		needs_ack = 1;
		tcph->ack_seq = htonl(ntohl(otcph->seq) + otcph->syn + otcph->fin 
				+ otcplen - (otcph->doff<<2));
		tcph->seq = 0;
	}

	/* Reset flags */
	((u_int8_t *)tcph)[13] = 0;
	tcph->rst = 1;
	tcph->ack = needs_ack;

	tcph->window = 0;
	tcph->urg_ptr = 0;

	/* Adjust TCP checksum */
	tcph->check = 0;
	tcph->check = tcp_v4_check(tcph, sizeof(struct tcphdr),
			nskb->nh.iph->saddr,
			nskb->nh.iph->daddr,
			csum_partial((char *)tcph,sizeof(struct tcphdr), 0));

	/* Adjust IP TTL, DF */
	nskb->nh.iph->ttl = MAXTTL;
	/* Set DF, id = 0 */
	nskb->nh.iph->frag_off = htons(IP_DF);
	nskb->nh.iph->id = 0;

	/* Adjust IP checksum */
	nskb->nh.iph->check = 0;
	nskb->nh.iph->check = ip_fast_csum((unsigned char *)nskb->nh.iph, 
			nskb->nh.iph->ihl);

	/* Routing: if not headed for us, route won't like source */
	if (ip_route_output(&rt, nskb->nh.iph->daddr, 
				local ? nskb->nh.iph->saddr : 0, 
				RT_TOS(nskb->nh.iph->tos) | RTO_CONN, 0) != 0)
		goto free_nskb;

	dst_release(nskb->dst);
	nskb->dst = &rt->u.dst;

	/* "Never happens" */
	if (nskb->len > nskb->dst->pmtu)
		goto free_nskb;

	printk(/*KERN_EMERG */"ZeSA warning: reset connection\n");
	connection_attach(nskb, oldskb->nfct);

	NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, nskb, NULL, nskb->dst->dev,
			ip_finish_output);
	return;

 free_nskb:
	kfree_skb(nskb);
}

static int
check_tcp(const struct iphdr *iph,
	  const struct tcphdr *tcph,
	  u_int16_t datalen)
{
	u_int8_t tcpflags;

	/* CHECK: Smaller than minimal TCP hdr. */
	if (datalen < sizeof(struct tcphdr))
		return 1;

	if (!tcph->source || !tcph->dest)
		return 1;

	/* CHECK: Smaller than actual TCP hdr. */
	if (datalen < tcph->doff * 4)
		return 1;

	/* CHECK: TCP reserved bits zero. */
	if(tcp_flag_word(tcph) & TCP_RESERVED_BITS)
		return 0;

	/* CHECK: TCP flags. */
	tcpflags = (((u_int8_t *)tcph)[13] & ~(TH_ECE|TH_CWR));
	if (tcpflags != TH_SYN 
			&& tcpflags != (TH_SYN|TH_ACK)
			&& tcpflags != TH_RST
			&& tcpflags != (TH_RST|TH_ACK)
			&& tcpflags != (TH_RST|TH_ACK|TH_PUSH)
			&& tcpflags != (TH_FIN|TH_ACK)
			&& tcpflags != TH_ACK
			&& tcpflags != (TH_ACK|TH_PUSH)
			&& tcpflags != (TH_ACK|TH_URG)
			&& tcpflags != (TH_ACK|TH_URG|TH_PUSH)
			&& tcpflags != (TH_FIN|TH_ACK|TH_PUSH)
			&& tcpflags != (TH_FIN|TH_ACK|TH_URG)
			&& tcpflags != (TH_FIN|TH_ACK|TH_URG|TH_PUSH))
		return 1;

	return 1;
}

static int 
packet_abnormal_check(const struct sk_buff *skb,
		struct iphdr *ip,
		struct tcphdr *tcp)
{
	int i, tcplen;
	struct rtable *rt;
	const u_int8_t *opt;

	tcplen = skb->len - ip->ihl*4;
	if(check_tcp(ip, tcp, tcplen))
		return 1;
	
	//!DF
	if(!(ntohs(ip->frag_off) & IP_DF))
		return 1;

	//checksum
	if(csum_tcpudp_magic(ip->saddr, ip->daddr, tcplen, IPPROTO_TCP, 
				csum_partial((char *)tcp, tcplen, 0)) != 0)
		return 1;

	//pmtu
	rt = (struct rtable*)skb->dst;
	if(/*rt->u.dst.pmtu < 576 || */skb->len > rt->u.dst.pmtu/*skb->dst->pmtu*/)
		return 1;
	
	//only valid mss for TCP SYN or SYN/ACK packets
	if(tcp->syn || (tcp->syn && tcp->ack))
	{
		opt = (u_int8_t *)tcp;
		for (i = sizeof(struct tcphdr); i < tcp->doff * 4; )
		{
			if ((opt[i] == TCPOPT_MSS) 
					&& ((tcp->doff * 4 - i) >= TCPOLEN_MSS) 
					&& (opt[i+1] == TCPOLEN_MSS))
			{
				u_int16_t mssval;
	
				mssval = (opt[i+2] << 8) | opt[i+3];
				if(skb->dst->pmtu < mssval || rt->u.dst.pmtu < mssval)//minimum MTU - 20 - 20
					return 1;
				break;
			}
			if (opt[i] < 2) i++;
			else i += opt[i+1]?:1;
		}
	}

	return 0;
}

static int
tiny_attack_assert(struct iphdr *iph,
		struct tcphdr *tcph)
{
	if((tcph->fin) && (tcph->syn) && (!tcph->rst) && (!tcph->psh) && (!tcph->ack) && (!tcph->urg))
		return 1;
	if((!tcph->fin) && (!tcph->syn) && (!tcph->rst) && (!tcph->psh) && (!tcph->ack) && (!tcph->urg))
		return 1;
	if(tcph->fin && tcph->syn && tcph->rst && tcph->psh && tcph->ack && tcph->urg)
		return 1;
	if((tcph->fin) && (!tcph->syn) && (!tcph->rst) && (tcph->psh) && (!tcph->ack) && (tcph->urg))
		return 1;
	if((tcph->syn) && (tcph->source == tcph->dest) && (iph->saddr == iph->daddr))
		return 1;
	if(ntohs(tcph->dest) == 139 && tcph->urg)
		return 1;
	if(tcph->ece && tcph->cwr)
		return 1;

	return 0;
}

static int
packet_cmds_match(const struct sk_buff *skb,
		struct iphdr *ip,
		struct tcphdr *tcp,
		char *haystack,
		char cmd[],
		unsigned char* cmdlen,
		int port)
{
	char *needle;
	unsigned char tail, head = 0;
	
	if(strlen(cmd) < 1) return 0;
	//only command from client direction, reduce cost of match
	if(port != 0 && ntohs(tcp->dest) != port) return 0;
	
	while((tail = cmd[head]))
	{
		needle = cmd + head + 1;
		*cmdlen = tail - head;
		head = tail;

		if(!memcmp(needle, haystack, *cmdlen - 1) && haystack[*cmdlen - 1] == ' ')
			return 1;
	}

	return 0;
}

static spinlock_t replace_lock = SPIN_LOCK_UNLOCKED;
static void replace_content(char *string, int sublen, char *replace)
{
	int len = strlen(replace);
	if(len > sublen) len = sublen;

	spin_lock_bh(&replace_lock);
	memset(string, ' ', sublen);
	memcpy(string, replace, len);
	spin_unlock_bh(&replace_lock);
}

int skip[BM_MAX_HLEN];
int shift[BM_MAX_HLEN];
/* Boyer Moore Sublinear string search - VERY FAST */
char *search_sublinear (char *needle, char *haystack, int needle_len, int haystack_len, char *replace) 
{
	int M1, right_end, sk, sh;  
	int ended, j, len[BM_MAX_HLEN];
	int i;
	char *pmatched = NULL;

	/* Setup skip/shift tables */
	M1 = right_end = needle_len-1;
	for (i = 0; i < BM_MAX_HLEN; i++) skip[i] = needle_len;  
	for (i = 0; (unsigned char)needle[i]; i++) skip[(unsigned char)(needle[i])] = M1 - i;  

	for (i = 1; i < needle_len; i++) {   
		for (j = 0; j < needle_len && needle[M1 - j] == needle[M1 - i - j]; j++);  
		len[i] = j;  
	}  

	shift[0] = 1;  
	for (i = 1; i < needle_len; i++) shift[i] = needle_len;  
	for (i = M1; i > 0; i--) shift[len[i]] = i;  
	ended = 0;  
	
	for (i = 0; i < needle_len; i++) {  
		if (len[i] == M1 - i) ended = i;  
		if (ended) shift[i] = ended;  
	}  

	/* Do the search*/  
	while (right_end < haystack_len)
	{
		for (i = 0; i < needle_len && haystack[right_end - i] == needle[M1 - i]; i++);  
		if (i == needle_len) {
			pmatched = haystack+(right_end - M1);
			if(replace != NULL)
				replace_content(pmatched, needle_len, replace);
			else
				return pmatched;
		}
		
		sk = skip[(int)(haystack[right_end - i])];  
		sh = shift[i];
		right_end = bm_max(right_end - i + sk, right_end + sh);  
	}

	return pmatched;
}  

/* Linear string search based on memcmp() */
char *search_linear (char *needle, char *haystack, int needle_len, int haystack_len, char *replace) 
{
	char *k = haystack + (haystack_len-needle_len);
	char *t = haystack;
	char *pmatched = NULL;
	
	while ( t <= k ) {
		if ( memcmp(t, needle, needle_len) == 0 )
		{
			pmatched = t;
			if(replace != NULL)
				replace_content(pmatched, needle_len, replace);
			else
				return pmatched;
		}
		t++;
	}

	return pmatched;
}

static int my_toupper(int what)
{
	if((what<'a')||(what>'z')) return what;
	return what-32;
}

static int my_isdigit(int what)
{
	if((what<'0')||(what>'9')) return 0;
	return 1;
}

static int my_isxdigit(int what)
{
	int i;
	if(my_isdigit(what)) return 1;
	if((what>='A')&&(what<='Z')) return 1;
	i=what-32;
	if((i>='A')&&(i<='Z')) return 1;
	return 0;
}

static int nibble(char what)
{
	if (my_isdigit((int)what)) return what - '0';
	if (my_isxdigit((int)what)) return my_toupper((int)what) - 'A' + 10;
	return -1;
}

char *urldecoded;
static void packet_data_resort(char **content, int *datalen, int port)
{
	int len = 0;
	char *head, *tail, *hyacinth;
	
	//first line of the packet, cause we only want command line
	//for HTTP and FTP, command line finished with ' '
	//for SMTP, we match full command line
	if(port != SMTP_PORT)
	{
		for(len = 0 ; len < *datalen && (*content)[len] != ' ' && (*content)[len] != '\n' && (*content)[len] != 0; len++);
		*datalen = len;
	}

	//Zero copy?!, hehe, but we have to translate encoded string --RR
	memcpy(urldecoded, *content, *datalen);
	
	head = hyacinth = *content = urldecoded;
	tail = head + *datalen;
	
	while(head < tail)
	{
		if((*head == '%') && (head < tail - 2))
		{
			//merge %XX
			if(my_isxdigit((int)*(head + 1)) && my_isxdigit((int)*(head + 2)))
			{
				*hyacinth = my_toupper((nibble(*(head + 1)) << 4) | nibble(*(head + 2)));
				head += 3; 
				hyacinth++;
				*datalen -= 2;
			}
			else
			{
				*hyacinth = my_toupper(*head);
				hyacinth++;
				head++;
			}
		}
		else
		{
			*hyacinth = my_toupper(*head);
			hyacinth++;
			head++;
		}
	}
	*hyacinth = 0;
}

static int
parse_string_match(char *content, char *strings, int datalen, char *replace, int bematch, int tag)
{
	char *needle;
	unsigned char head, tail;
	unsigned char sublen;
	proc_ipt_search search;
	int ismodify = 0;

	head = 0;
	while((tail = strings[head]))
	{
		needle = strings + head + 1;
		sublen = tail - head - 1;
		head = tail;
		
		//only http will match begin or end
		if(bematch && *needle == MATCH_BEGIN)
		{
			needle++;
			sublen--;
			if(sublen < 1) return 0;//skip '+' '-' only string
			if(memcmp(needle, content + 1, sublen) == 0)//skip '/'
				return 1;
		}
		else if(bematch && *needle == MATCH_END)
		{
			needle++;
			sublen--;
			if(sublen < 1) return 0;//skip '+' '-' only string
			if(memcmp(needle, content + datalen - sublen, sublen) == 0)
				return 1;
		}
		else
		{
			search = search_linear;
#if 0
			if((datalen > IPT_STRING_HAYSTACK_THRESH) && (sublen > IPT_STRING_NEEDLE_THRESH)) 
			{
				if(datalen < BM_MAX_HLEN) 
					search = search_sublinear;
				else if (net_ratelimit())
					printk(KERN_INFO "ZeSA error: packet too big to attempt,
							sublinear string search (%d bytes)\n", datalen );
			}
#endif
			if(tag)
			{
				int i;
				char tagstr[32] = "";

				if(sublen > 29)
				{
					printk(KERN_EMERG "ZeSA error: tag string can not longer than 29\n");
					return 0;
				}
				
				memset(tagstr, 0, 32);
				tagstr[0] = '<';
				for(i = 1 ; i < sublen + 1 ; i++) tagstr[i] = *(needle + i - 1);
				if(search(tagstr, content, sublen + 1, datalen, "<!--") != NULL)
					ismodify = 1;
	
				memset(tagstr, 0, 32);
				tagstr[0] = '<'; tagstr[1] = '/';
				for(i = 2 ; i < sublen + 2 ; i++) tagstr[i] = *(needle + i - 2);
				tagstr[sublen + 2] = '>';
				if(search(tagstr, content, sublen + 3, datalen, "-->") != NULL)
					ismodify = 1;

				if(search(needle, content, sublen, datalen, " ") != NULL)
					ismodify = 1;
			}
			else
			{	
				if(search(needle, content, sublen, datalen, replace) != NULL)
				{
					if(replace == NULL)
						return 1;
					else
						ismodify = 1;
				}
			}
		}
	}
	
	return ismodify;
}

static int
cmd_params_match(const struct sk_buff *skb,
		struct iphdr *ip,
		struct tcphdr *tcp,
		char *fullcmd,
		char params[],
		unsigned char cmdlen,
		int port)
{
	int datalen;

	if(strlen(params) < 1) return 0;
	
	fullcmd = fullcmd + cmdlen;
	datalen = skb->len - ip->ihl*4 - tcp->doff*4 - cmdlen;
	packet_data_resort(&fullcmd, &datalen, port);

	if(parse_string_match(fullcmd, params, datalen, NULL, (port == HTTP_PORT)?1:0, 0))
		return 1;
	
	return 0;
}

static int
packet_content_filter(const struct sk_buff *skb,
		struct iphdr *ip,
		struct tcphdr *tcp,
		char strings[],
		char* replace,
		int tag)
{
	if(strlen(strings) < 1) return 0;

	if(parse_string_match((char *)tcp + tcp->doff*4, strings, skb->len - ip->ihl*4 - tcp->doff*4, replace, 0, tag))
	{
		if(tag || replace != NULL)
		{
			/* Adjust TCP checksum */
			tcp->check = 0;
			tcp->check = csum_tcpudp_magic(ip->saddr, ip->daddr, 
					skb->len - ip->ihl*4, IPPROTO_TCP,
					csum_partial((char *)tcp, skb->len - ip->ihl*4, 0));

			/* Adjust IP checksum */
			skb->nh.iph->check = 0;
			skb->nh.iph->check = ip_fast_csum((unsigned char *)skb->nh.iph, 
					skb->nh.iph->ihl);
			return 0;
		}
		return 1;
	}
	
	return 0;
}

static int
modify_HTTP_request_header(const struct sk_buff *skb,
		struct iphdr *ip,
		struct tcphdr *tcp,
		int port,
		char *needle)
{
	int sublen = strlen(needle);
	int datalen = skb->len - ip->ihl*4 - tcp->doff*4;
	char *t = (char *)tcp + tcp->doff*4;
	char *k = t + datalen - sublen;
	int ismodify = 0;
	int http_port = HTTP_PORT;
	
	if(port != 0) http_port = port;
	if(ntohs(tcp->dest) != http_port || memcmp(t, "GET ", 4) != 0) 
		return 0;
	
	while(t <= k)
	{
		if(memcmp(t, needle, sublen) == 0)
		{
			int i = 0;
			t = t + sublen;
			while(t + i <= k)
			{
				i++;
				if(*(t + i) == '\r' && *(t + i + 1) == '\n')
					break;
			}
			memset(t, ' ', i);
			
			ismodify = 1;
			break;
		}
		t++;
	}
	
	if(ismodify)
	{
		/* Adjust TCP checksum */
		tcp->check = 0;
		tcp->check = csum_tcpudp_magic(ip->saddr, ip->daddr, 
				skb->len - ip->ihl*4, IPPROTO_TCP,
				csum_partial((char *)tcp, skb->len - ip->ihl*4, 0));

		/* Adjust IP checksum */
		skb->nh.iph->check = 0;
		skb->nh.iph->check = ip_fast_csum((unsigned char *)skb->nh.iph, 
				skb->nh.iph->ihl);
		return 1;
	}
	else
		return 0;
}

static int
modify_HTTP_response_header(const struct sk_buff *skb,
		struct iphdr *ip,
		struct tcphdr *tcp,
		int port,
		char *needle)
{
	int sublen = strlen(needle);
	int datalen = skb->len - ip->ihl*4 - tcp->doff*4;
	char *t = (char *)tcp + tcp->doff*4;
	char *k = t + datalen - sublen;
	int ismodify = 0;
	int http_port = HTTP_PORT;
	
	if(port != 0) http_port = port;
	if(ntohs(tcp->source) != http_port || memcmp(t, "HTTP", 4) != 0) 
		return 0;
	
	while(t <= k)
	{
		if(memcmp(t, needle, sublen) == 0)
		{
			int i = 0;
			t = t + sublen;
			while(t + i <= k)
			{
				i++;
				if(*(t + i) == '\r' && *(t + i + 1) == '\n')
					break;
			}
			memset(t, ' ', i);
			
			ismodify = 1;
			break;
		}
		t++;
	}
	
	if(ismodify)
	{
		/* Adjust TCP checksum */
		tcp->check = 0;
		tcp->check = csum_tcpudp_magic(ip->saddr, ip->daddr, 
				skb->len - ip->ihl*4, IPPROTO_TCP,
				csum_partial((char *)tcp, skb->len - ip->ihl*4, 0));

		/* Adjust IP checksum */
		skb->nh.iph->check = 0;
		skb->nh.iph->check = ip_fast_csum((unsigned char *)skb->nh.iph, 
				skb->nh.iph->ihl);
		return 1;
	}
	else
		return 0;
}

static int
match(const struct sk_buff *skb,
		const struct net_device *in,
		const struct net_device *out,
		const void *matchinfo,
		int offset,
		const void *hdr,
		u_int16_t datalen,
		int *hotdrop)
{
	struct ipt_ZeSA_info *info = (struct ipt_ZeSA_info *)matchinfo;
	struct iphdr *ip;
	struct tcphdr *tcp;
	char *haystack;
	char *replace = NULL;
	unsigned char cmdlen = 0;

	ip = skb->nh.iph;
	if(!ip || ip->protocol != 6) return 0;
	
	tcp = (struct tcphdr *)((char *)ip + ip->ihl*4);
	if(info->port != 0 && ntohs(tcp->source) != info->port && ntohs(tcp->dest) != info->port)
	{
		if(info->port == FTP_PORT)
		{
			if(strlen(info->content) < 1)
				return 0;
			else
			{
				enum ip_conntrack_info ctinfo;
				struct ip_conntrack *ct;
//				struct ip_ct_ftp *ftpinfo;

				//not finished yet, maybe continue it later
				ct = ip_conntrack_get((struct sk_buff *)skb, &ctinfo);
//				ftpinfo = &ct->help.ct_ftp_info;
//				return 0;
			}
		}
		else
			return 0;
	}
	
	if(info->encoding)
		modify_HTTP_request_header(skb, ip, tcp, info->port, HTTP_ACCEPT_ENCODE);
	if(info->cookie)
		modify_HTTP_response_header(skb, ip, tcp, info->port, HTTP_SET_COOKIE);
	if(info->cookie || info->encoding)
		return 0;

	if(packet_abnormal_check(skb, ip, tcp) || tiny_attack_assert(ip, tcp))
	{
		if(info->rst) send_reset(skb, 0);
//		*hotdrop = 1;
//		return 0;
	}
	
	haystack = (char *)tcp + tcp->doff*4;
	/* match cmd(param) or content, only one of them we supported in phase I */
	if(packet_cmds_match(skb, ip, tcp, haystack, info->cmd, &cmdlen, info->port))
	{
		if(strlen(info->param) < 1) return 1;
		if(cmd_params_match(skb, ip, tcp, haystack, info->param, cmdlen, info->port))
			return 1;
	}

	if(strlen(info->replace) > 0) replace = info->replace;
	if(packet_content_filter(skb, ip, tcp, info->content, replace, info->tag))
		return 1;

	return 0;
}

static int
checkentry(const char *tablename,
		const struct ipt_ip *ip,
		void *matchinfo,
		unsigned int matchsize,
		unsigned int hook_mask)
{
	if (matchsize != IPT_ALIGN(sizeof(struct ipt_ZeSA_info)))
		return 0;

	return 1;
}

static struct ipt_match ZeSA_match
= { { NULL, NULL }, "ZeSA", &match, &checkentry, NULL, THIS_MODULE };

static int __init init(void)
{
	int ret;
	
	urldecoded = kmalloc(STR_MAX_LEN, GFP_KERNEL);
	if(!urldecoded)
		return -ENOMEM;
	
	ret=ipt_register_match(&ZeSA_match);
	if(ret < 0)
	{
		kfree(urldecoded);
	}
	return ret;
}

static void __exit fini(void)
{
	ipt_unregister_match(&ZeSA_match);
	kfree(urldecoded);
}

module_init(init);
module_exit(fini);
